查看原文
其他

Meta实践分享:生成式AI对存储系统的冲击与应对

常华Andy Andy730
2025-01-01

Robin Battey(Meta)

这幅图展示了一项关于AI相关研究论文发表速度的调查。我们可以看到图表的右侧显示了2023年的数据。在过去一年里,有超过3000篇相关论文发表,相当于每天平均有超过100篇AI论文问世。值得注意的是,这个数字还在持续增长。

现在,让我们走进我们这些“苦命”的存储工程师的世界。在这个领域,进展似乎异常缓慢,我们仍然在使用上世纪80年代的硬盘技术。尽管扩展存储规模的需求日益迫切,但我们面临着前所未有的巨大挑战。而且,我们的扩展方式也与众不同。

过去,大规模存储需求主要来自数据仓库应用,这类应用的I/O操作恒定且为流式,启动和关闭速度都相对较慢。数据仓库应用包含许多独立的工作任务,因此整体性能主要取决于吞吐量(可以理解为平均延迟与数据量大小的关系),异常值通常可以忽略不计。由于存储本身是价格高昂且难以变化的,为了满足项目的整体需求,我们通常建议用户修改代码以适应存储的环境。

然而,生成式AI训练的出现彻底改变了游戏规则。I/O操作就像“迎面撞上墙的列车”般猛烈袭来,现在许多依赖任务在检查点摄取模型中进行排队。这导致最大延迟(即异常值)成为影响整体性能的关键因素。众所周知,与当今GPU的高昂成本相比,存储成本微不足道,更不用说产品上市的时间成本了。因此,从整体来看,让存储适应AI任务的需求才是更明智的选择。

这里简要介绍了从存储角度来看的生成式训练过程。我们主要关注的是存储的读取和写入操作。简而言之,整个过程就像这样:GPU不断地从存储中读取训练数据,然后在循环中进行处理(就像施展魔法一样),偶尔会暂停保存当前状态,并一直重复这个循环直到训练完成。

数据摄取完全是读取操作:GPU从存储中读取训练数据,进行AI处理,然后读取下一部分训练数据,周而复始。对于Llama 3来说,这并不是最具挑战性的部分,即使需要读取大量文本数据,对于我们的存储系统来说也并不会造成太大负担。

与之相反,检查点操作则全是写入操作。所谓的检查点,就是为了在发生故障时能够恢复训练而保存的训练状态。在进行检查点操作时,训练会暂停,GPU会将所有状态信息转储到存储设备中,然后重新启动训练。显然,我们既想尽量缩短训练暂停的时间,又想减少因故障造成的训练进度损失,因此需要频繁地进行检查点操作。

GPU训练集群实际上相当于一台庞大的超级计算机,网络则充当了其内部高速互联通道(PCI总线)的一部分。任何一台机器、任何一块GPU或内存条发生故障,都会导致整个集群崩溃,直到重新加载之前保存的训练状态。随着GPU增多,也意味着潜在的故障点也越多,所以每次故障造成的训练进度损失可能更大,也需要更加频繁地进行检查点操作。

这正是我们的难点所在,尤其是在扩展到更大模型和更多GPU时更是如此。整个过程就像这样:存储网络在一瞬间从零流量状态变成完全饱和,所有设备和队列都在10毫秒内被填满。我们曾经对这种情况进行过测试。更糟糕的是,由于每个工作节点都需要转储其整个状态,任何一个运行缓慢的节点都会拖慢整个检查点操作的速度。与大多数其他大规模应用不同,这里的关键指标是最大延迟(即异常值)。而这种情况恰恰发生在你所能想象到的最恶劣的存储环境下:一个巨大的流量峰值试图使用超出系统所能提供的存储容量。我们需要确保整个检查点操作能够顺利完成。

那么,我们是否要求他们改变使用方式以适应存储需求呢?答案是否定的。在AI领域,出现了一个新的“摩尔定律”:每24个月,AI研究的速度就会翻一番,每天这个领域都会出现超过100个值得关注的变化。而且,这还是去年的数据!新的AI实验的迭代速度才是我们优化的关键。当我们的AI工程师从与某个研究论文相关的GitHub仓库中获取新内容时,我们需要确保整个流程能够顺利高效地进行。

这里有一个分工明确的图(希望没有争议):简而言之,AI工程师负责AI研究,存储工程师专注于存储系统。如果需要花费一个小时来适配其他同事的GitHub代码仓库以符合存储的API,那就已经太浪费时间了。但实际上,适配工作通常需要几天才能完成,并且还需要持续维护,这显然是不可接受的。

业界AI开发人员普遍遵循这些存储标准,我们必须满足他们的需求。即使这些API本身可能并不完美,但它们已经是业界标准,因此我们必须提供相应的支持。我们希望我们的AI研究人员能够将精力放在AI研究上,而不是被存储问题所困扰。

不过,那些不是我们现有的存储解决方案。我们内部研发了一套名为TectonicFS的系统,从一开始就充分考虑了MapReduce的特性。目前,TectonicFS已经部署在Meta所有主要存储节点之上,其最大I/O吞吐量远超AI的需求。然而,尽管TectonicFS具备一些契合我们需求的特性,例如支持追加写模式以满足检查点需求,但遗憾的是,其最后两个关键特性未能满足我们的实际应用场景。

因此,我们陷入了两难的境地:面临问题却迟迟找不到合适的解决方案。我们测试了许多产品(名称略去以保护隐私),评估了众多方案,包括那些号称专为AI工作负载设计的现成产品。问题在于,Meta的规模体量过于庞大,这些现成产品虽能勉强应对LLaMA 2的规模,却无法满足LLaMA 3的更高需求,更不用说我们对LLaMA 4的期望了。

最终,我们意识到,我们需要一种前所未有的存储解决方案,既要高效运行,又要兼容NFS的接口。简单来说,我们面临两个选择:要么提升现有NFS方案的扩展性,要么让可扩展的TectonicFS兼容NFS接口。

对于参加过“Systems @Scale”这类会议的人来说,这应该不足为奇:通常在现有可扩展架构上添加一个API接口,要比在现有API上添加可扩展架构更加容易。因此,我们决定使用FUSE封装器来包装TectonicFS,使其能够像挂载NFS一样进行使用。

请记住这张幻灯片:所有GPU会周期性地同时将各自的状态转储到存储系统中。有没有注意到那个跨越8个数量级的惊人范围了吗?最高值和最低值之间相差足足4000倍!这可不仅仅是“我们无法透露真实数据”的借口,这个范围确实令人难以置信。我们经常会收到这样的消息:“哦,对了,我们昨天晚上将检查点大小增加了60倍。”这让我们整个存储团队都惊呆了。

过去,我们一直遵循这样的模式:获取客户(在本例中是AI团队)的需求,然后根据最大需求来构建系统。如果他们的需求超出设计范围,那将由他们自己承担后果。然而,现在这种方式已经行不通了。由于AI行业飞速发展,数据规模的增长趋势不仅是我们无法预测的,就连AI团队自己也无法明确预估。我们唯一能做的,就是做好应对极限情况的准备。这种思维方式的转变对我们来说非常具有挑战性。

因此,我们以闪电般的速度搭建了这个系统,一路打破了许多既有预期,克服了重重困难,最终实现了端到端测试。当然,在大规模真实GPU硬件上进行实际测试成本实在太高,所以我们开发了一种方法来模拟AI工作负载的存储需求。具体而言,我们运行了完整的训练工作负载,但使用随机数据代替了实际的GPU计算数据。这种方法就好比用“垃圾数据”来填充整个系统。在所有测试集群上运行后,一切看起来都非常顺利。然而,就在我们准备实际部署时,检查点的大小却在一夜之间暴增了70倍,让我们措手不及。

事实证明,虽然TectonicFS可以处理任何规模的数据,但如果真的触及了它的性能上限,整个系统就会崩溃。吞吐量急剧下降到大约5%,并且会持续数小时,这确实让人尴尬。不过请允许我们辩解一下,在此之前从未有任何系统真正达到过如此庞大的数据规模。存储需求的变化通常比较缓慢,我们习惯于提前扩展集群以满足未来的需求,因此我们从未真正触及过系统的上限。

Sumit Gupta(Meta)

当AI流量的出现,带来了其独有的需求。为了解决这些新需求,让我们回顾一下我们所采取的措施。在AI流量出现之前,我们已经在大型共享集群上运行着来自多租户的流量。然而,AI的兴起也带来了AI检查点机制。在检查点过程中,所有GPU都将暂停工作,并以一种高度同步的方式将内存中的状态转储到持久化存储器中。正如右侧图表所示,这种同步操作会产生巨大的存储需求峰值,这些峰值开始干扰其他租户的运行,导致他们无法满足“延迟”的服务级别协议(SLA)的要求。

通常情况下,我们会使用速率限制器(Rate Limiter)来应对这类问题。但是,对于AI流量而言,速率限制器的响应速度有些不够快。为什么会这样呢?问题在于AI流量会迅速激增,通常在毫秒级别内就能达到峰值,而我们的速率限制器只能以秒为单位进行响应,无法适应这种突发性的流量变化。这暴露了我们在租户隔离方面存在的一些不足之处。为了解决这些问题,我们一方面着手并行优化租户隔离,另一方面则更进一步,决定构建专用的AI集群。

解决完租户隔离问题后,下一个需要关注的方面是IO供给。我们需要明确AI工作负载对IO的需求量。现有的集群设计适用于支持大多数通用的IO模式,以满足不同租户的需求。有些租户运行小块IO以追求低延迟,而另一些租户则运行大块IO以追求高吞吐量。正是由于这种多样性,我们并没有针对每TB数据进行最佳带宽调优。通过观察AI流量,我们发现AI工作负载更看重带宽,而非延迟。实际上,AI工作负载每TB数据所需的带宽比现有集群提供的要高出两到三倍。

由于我们已经决定构建专用的AI集群,这个问题迎刃而解。我们可以重新定义这些工作负载,确保仅支持AI特定的模式。这样一来,我们可以将整体IO供给提升四倍,从而满足AI的需求。

当我们开始运行一些初始示例工作负载时,有效吞吐量(Goodput)出现了一些有意思的现象:尽管集群的整体吞吐量很高,但有效吞吐量却开始下降。这是怎么回事呢?经过深入分析流量数据,我们发现问题出在检查点机制上。当检查点启动时,它会向集群推送大量IO请求,数量有时甚至达到数千个。这些请求在集群上堆积,最终让集群不堪重负。随着队列长度不断增加,失败的请求数量也随之攀升。虽然底层集群的吞吐量表现良好,但上层的有效吞吐量却并不理想,这表明我们浪费了大量的IO资源。

为了深入探究问题所在,我们开始进行调试。发现IO超时是因为底层的队列堆积过深导致的。由于集群设计初衷是提供低延迟服务,所以当某些IO请求在一秒钟内没有返回时,系统就会启动重试机制,并且将这些超时请求标记为失败。然而,这种重试反而导致了更多的IO请求涌入队列,进一步加剧了拥塞问题。总的来说,底层虽然努力工作,提供了高QPS和高吞吐量,但上层却无法有效利用这些资源,反而出现了吞吐量下降和失败率上升的现象。因此,我们意识到需要尽早拒绝这些超时的IO请求。

为了实现这一目标,我们采用了两种策略:控制延迟和自适应后进先出(LIFO)。

控制延迟:如果某个队列在过去10毫秒内一直没有清空,那么我们希望队列中的IO请求停留时间大约为N毫秒(N远小于总允许延迟时间)。这将使我们能够更快地清空队列,避免IO请求因为等待时间过长而失效。

自适应后进先出(LIFO):当队列中堆积了大量过时IO请求时,我们将不再遵循先进先出(FIFO)的处理原则,而是优先处理较新的IO请求。过时的IO请求会被标记为超时,然后在其他地方重新尝试。

采用了这些策略之后,队列变得空旷许多,整体吞吐量和有效吞吐量都得到了显著提升。我们设置了两种队列超时时间:对于正常的IO请求,超时时间为1秒;而对于过时的IO请求,超时时间会降低到100毫秒。接下来,我们会逐步降低队列超时时间,只要保持在100毫秒以上,就将其恢复到1秒。

这一调整带来了以下好处:
  1. 优化P99延迟:减少了IO请求的超时和重试,有效降低了成功IO的延迟。
  2. 提升有效吞吐量:由于不再浪费时间重试或处理失败的IO请求,整体吞吐量得到提升。
  3. 避免热点问题:过时的IO请求可以重试到其他节点,实现更好的集群负载均衡。
  4. 增强可扩展性:如果需要更高的IO处理能力,只需添加更多节点即可扩展集群。能够更准确地预测给定AI需求所需节点数量。

在这个过程中,我们还发现了集群中可以进一步优化的其他领域。这些额外的发现可以被看作是调优过程中的副作用或附加问题:
  • 节点的内存带宽饱和:因为网络流量的激增会增加内存带宽的消耗。为了限制内存带宽的消耗,我们可以限制网络流量。
  • 内核交接时间过长:内核交接时间是指将完成的I/O操作交还给内核以便其传回客户端所需的时间。当网络拥塞时,大量数据会滞留在内核中,导致节点内存不足报错。为了避免这种情况,我们开始限制内核交接时间。

通过限制网络流量和内核交接时间这两个参数,我们发现还可以调整其他的一些参数,从而改善资源预留和整体的节点运行。

为了更好地评估调优的效果,我们还构建了一个工具来模拟类似于AI工作负载的流量。经过分析,我们意识到所有这些问题都源于缺乏良好的流量控制和速率限制机制。

因此,我们开始着手实施流量控制并在前端限制I/O操作。

Robin Battey(Meta)

经过一番努力,我们现在能够应对毫秒级内从零流量激增到足以压垮整个集群的挑战。对于任何AI领域可能出现的难题,我们都已做好了充分的准备。另外,如果能够在本地机器上挂载一个像NFS一样易于使用且高度可扩展的分布式文件系统,将极大地降低非AI项目的准入门槛。事实上,我们现在已经收到了来自内部许多项目的咨询请求。

然而,这次经历带给我们最深远的改变或许是对大规模存储系统的认知转变。过去,出于业务考量,我们不得不龟缩于传统框架之内,被动地应对行业缓慢的变化,进行历经数年的迭代改进。现在,我们已经站在了行业的前沿,引领着技术发展的潮流。如果未来需要放慢脚步,那也绝不会是因为存储系统落后于时代。



--【本文完】---

近期受欢迎的文章:

  1. Meta:大规模AI基础设施

  2. Meta的AI网络演进

  3. 微软CEO和CTO访谈:AI平台转型

  4. Meta大规模AI集群内部揭秘:构建60万个H100的强大算力

  5. Azure下一代块存储架构:深度技术解析



更多交流,可添加本人微信

(请附姓名/单位/关注领域)

继续滑动看下一个
Andy730
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存